What the Non-text Contrast Check Looks At
Before the tool can measure whether an icon is visible enough, it has to decide which things on the page count as "icon-only controls". That shortlist is the only thing the Non-text Contrast measurement ever examines.
It is not a fixed list of element types written down once. It is built from scratch, on every page, on every run. Think of it as the guest list for a club: the door staff build it fresh each night. Whoever is on the list gets measured. Whoever is not does not exist as far as the check is concerned.
Understanding how that list is built is the single best predictor of what the check will and will not find.
How the list is built: a funnel with four filters
Filter 1 — The raw pool
The tool asks the browser for its accessibility tree — the same description of the page a screen reader uses. From that, a thing stays in the running if either:
- it can receive keyboard focus, or
- it is labelled as a button, link, tab, menu item or search.
⚠️ If a candidate cannot be matched back to something real on the page, it is dropped without a word — and everything nested inside it is dropped too. One unresolvable container can silently take a whole region of the page off the list.
Filter 2 — "Does it have words in it?"
Every candidate is then judged by a mechanical rule. It stays only if it is visible and has no meaningful words inside it. "No meaningful words" means:
- nothing at all, or
- one or two characters with no letters —
0,11,+,-(read as badges, not labels), or - an icon-font code name — text shaped like
keyboard_arrow_rightorchevron_left: all lowercase, joined by underscores. On screen these render as a picture, not as text.
If it does have words, it gets one second chance: does it, or anything inside it, use a recognised icon font? That list is hand-maintained and contains exactly eight families — Material Icons and Material Symbols, Font Awesome, Glyphicons, IcoMoon, Ionicons, Dashicons, Genericons, Typicons. If yes, it stays.
Everything that fails this filter is gone. The AI never even hears about it.
Filter 3 — The AI's vote
A picture of the whole page is taken with a red outline drawn around each survivor, and sent to a vision model. The question is narrow: "which of these red boxes contain no visible text inside the box?" — plus two side questions per element: is this a photo or illustration? and is this a brand logo?
The model is told, in capitals, "if in doubt, report it — we prefer false positives to missed issues."
The answer is saved against a snapshot of the page, so an identical page does not pay for a second AI call.
Filter 4 — The double-check back on the real page
Everything the AI named is re-examined against the live page and thrown out if:
- it cannot be matched back to something real on the page;
- it is a photo or illustration (excluded by default);
- it is a brand logo (excluded by default — unless it is itself a link or a button, in which case it is kept);
- it is not a button, a link, or something labelled button / link / tab;
- it is a native form field;
- it is fully transparent;
- the "no meaningful words" rule fails on a second reading;
- everything visible inside it is an image — a classic image-link, treated as an alt-text topic rather than a contrast one.
What survives is the final list.
So: what is on the list, and what never is
On the list
- Icon-only buttons, links and tabs: hamburger menus, chevrons and arrows, close ✕, search magnifier, share and social icons.
- Icons drawn as vector graphics, or via one of the eight recognised icon fonts.
- Controls whose only "text" is a small number badge (
0,11) or a bare symbol (+). - A logo — but only when the logo is itself the link or the button.
Never on the list
- Anything with real words in it, even if it also contains a nearly invisible icon (
Download ⬇is discarded whole). - Photos, illustrations, product images, thumbnails (off by default).
- Standalone logos that are not links or buttons.
- Image-only links — a link wrapping just a picture.
- Fully transparent controls.
- Non-interactive graphics — a status icon, a chart, an icon sitting next to a paragraph. These can be a real accessibility problem too, but they are outside this check by design.
- Anything not on screen when the picture is taken — inside a closed menu, an unopened modal, a later carousel slide.
- Anything the AI simply did not mention in its reply.
Why this matters for reading results
The measurement step does no searching of its own. It opens the list and works through it. If the list is empty, it reports success and stops, with the message "No graphic elements to check for non-text contrast."
That has one consequence worth stating bluntly:
🔴 The accuracy of Non-text Contrast is capped by the quality of this list.
A pass is not a statement that the page is fine — it can equally mean nothing made it onto the list. There is no path by which a control that missed the list gets reported later.
Concretely:
1. The filters run in a fixed order, and the AI is third. The mechanical "does it have words" rule runs before the AI. So the instruction to "report if in doubt" only ever applies to what survived the mechanical rule — it cannot rescue anything the rule already dropped. All that generous, coverage-first tuning sits downstream of the strictest filter.
2. The icon-font list is eight hand-maintained names. A site using a custom or self-hosted icon font that is not one of those eight, and whose code name does not happen to match the lowercase_with_underscores shape (say icon-close), has its icons read as real text — and the control is dropped. Adding an icon font to that list directly increases coverage; leaving one off directly loses coverage.
3. Two different definitions of "interactive", and they do not match. The raw pool welcomes anything focusable, plus menu items and search. The final double-check only accepts buttons, links and tabs. So an icon-only menu item, or a focusable custom widget, is collected, outlined, photographed, sent to the AI and paid for — and then thrown away at the end. Cost with no coverage in return.
4. Failures are silent, and they always fail towards "pass". Whenever something cannot be resolved or a style cannot be read, the element is quietly dropped rather than flagged as "could not check". The deliberate choice is "assume not an issue on error, to avoid false positives" — which means the realistic risk profile of this check is missed issues, not false alarms, despite the AI step being tuned the other way.
5. The list is only valid for the page exactly as it was when the picture was taken. Anything that appears after an interaction is not on it.
One thing worth verifying
When a cached AI answer is reused — the same page seen before — the step that paints the red outlines is skipped. That is also the step that builds the map from the AI's answer back to real elements on the page.
If that map is empty on a cache hit, every cached finding would fail the "cannot be matched back to something real" test in Filter 4, leaving the list empty. That would mean a second run of the same page silently reports no issues.
This has not been confirmed against a live page. Treat it as "looks like a real gap, please verify with a repeat run on the same URL" rather than as established behaviour.
